package com.star.files.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.star.common.enums.BizStatus;
import com.star.common.exception.BizException;
import com.star.files.config.FilesProperties;
import com.star.files.enums.FileCategory;
import com.star.files.enums.MimeType;
import com.star.files.service.FilesService;
import io.minio.*;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 文件管理服务impl
 *
 * @author yangzj
 */
@Slf4j
@Service
public class FilesServiceImpl implements FilesService {

    @Autowired
    private MinioClient client;
    @Resource
    private FilesProperties filesProperties;

    @Override
    public String upload(MultipartFile file, String fileCategory, Boolean keepFileName) {
        Assert.notNull(file, "文件不能为空");
        if (!FileCategory.contains(fileCategory)) {
            throw new BizException(BizStatus.FILE_CATEGORY);
        }
        return this.saveFile(file, fileCategory, keepFileName);
    }

    @Override
    public List<String> uploads(MultipartFile[] files, String fileCategory, Boolean keepFileName) {
        List<String> filePaths = new ArrayList<>();
        for (MultipartFile file : files) {
            filePaths.add(this.upload(file, fileCategory, keepFileName));
        }
        return filePaths;
    }

    @Override
    public String upload(String url, String fileCategory, Boolean keepFileName) {
        return this.upload(this.urlToMultipartFile(url), fileCategory, keepFileName);
    }

    @Override
    public List<String> uploads(List<String> urls, String fileCategory, Boolean keepFileName) {
        return urls.stream().map(url -> this.upload(url, fileCategory, keepFileName)).collect(Collectors.toList());
    }

    @Override
    public void delete(String filePath, boolean isThrowException) {
        try {
            client.removeObject(RemoveObjectArgs.builder().bucket(filesProperties.getBucketName()).object(filePath).build());
        } catch (Exception e) {
            if(isThrowException) {
                throw new BizException(BizStatus.FILE_DELETE_FAIL);
            }else {
                log.error("文件删除失败, filePath={}", filePath, e);
            }
        }
    }

    @Override
    public void download(String filePath, HttpServletRequest request, HttpServletResponse response) {
        String contentType = request.getServletContext().getMimeType(filePath);
        // 视频用流播放
        boolean isVideo = contentType.contains("video");
        if (isVideo) {
            this.playVideo(filePath, request, response);
            return;
        }
        InputStream in = null;
        ServletOutputStream out = null;
        try {
            in = this.getObject(filesProperties.getBucketName(), filePath);
            if (in == null) {
                throw new BizException(BizStatus.FILE_NOT_EXIST);
            }
            out = response.getOutputStream();
            response.setCharacterEncoding("UTF-8");
            response.setContentType(StrUtil.isBlank(contentType) ? "application/octet-stream" : contentType);
            // 写到out
            IoUtil.copy(in, out);
        } catch (Exception e) {
            log.error("文件下载异常", e);
            throw new BizException(BizStatus.FILE_IO_EXCEPTION);
        } finally {
            IoUtil.close(in);
            IoUtil.close(out);
        }
    }


    /**
     * 播放视频
     *
     * @param filePath 文件路径
     * @param request  request
     * @param response 响应
     */
    private void playVideo(String filePath, HttpServletRequest request, HttpServletResponse response) {
        try {
            String range = request.getHeader("Range");
            //获取文件信息
            StatObjectResponse statObjectResponse = client.statObject(
                    StatObjectArgs.builder().bucket(filesProperties.getBucketName()).object(filePath).build());
            System.out.println(statObjectResponse);
            //开始下载位置
            long startByte = 0;
            //结束下载位置
            long endByte = statObjectResponse.size() - 1;
            log.info("文件开始位置：{}，文件结束位置：{}，文件总长度：{}", startByte, endByte, statObjectResponse.size());

            //有range的话
            if (StrUtil.isNotBlank(range) && range.contains("bytes=") && range.contains("-")) {
                range = range.substring(range.lastIndexOf("=") + 1).trim();
                String[] ranges = range.split("-");
                try {
                    //判断range的类型
                    if (ranges.length == 1) {
                        //类型一：bytes=-2343
                        if (range.startsWith("-")) {
                            endByte = Long.parseLong(ranges[0]);
                        }
                        //类型二：bytes=2343-
                        else if (range.endsWith("-")) {
                            startByte = Long.parseLong(ranges[0]);
                        }
                    }
                    //类型三：bytes=22-2343
                    else if (ranges.length == 2) {
                        startByte = Long.parseLong(ranges[0]);
                        endByte = Long.parseLong(ranges[1]);
                    }

                } catch (NumberFormatException e) {
                    startByte = 0;
                    endByte = statObjectResponse.size() - 1;
                    log.error("Range Occur Error, Message:" + e.getLocalizedMessage());
                }
            }
            //要下载的长度
            long contentLength = endByte - startByte + 1;
            // 文件类型
            String contentType = request.getServletContext().getMimeType(filePath);
            // 解决下载文件时文件名乱码问题
            byte[] fileNameBytes = filePath.getBytes(StandardCharsets.UTF_8);
            filePath = new String(fileNameBytes, StandardCharsets.ISO_8859_1);

            //各种响应头设置
            //支持断点续传，获取部分字节内容：
            response.setHeader("Accept-Ranges", "bytes");
            //http状态码要为206：表示获取部分内容
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            response.setContentType(contentType);
            response.setHeader("Last-Modified", statObjectResponse.lastModified().toString());
            //inline表示浏览器直接使用，attachment表示下载，fileName表示下载的文件名
            response.setHeader("Content-Disposition", "inline;filename=" + filePath);
            response.setHeader("Content-Length", String.valueOf(contentLength));
            //Content-Range，格式为：[要下载的开始位置]-[结束位置]/[文件总大小]
            response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + statObjectResponse.size());
            response.setHeader("ETag", "\"".concat(statObjectResponse.etag()).concat("\""));

            GetObjectResponse stream = client.getObject(
                    GetObjectArgs.builder()
                            .bucket(statObjectResponse.bucket())
                            .object(statObjectResponse.object())
                            .offset(startByte)
                            .length(contentLength)
                            .build());
            BufferedOutputStream os = new BufferedOutputStream(response.getOutputStream());
            byte[] buffer = new byte[1024];
            int len;
            while ((len = stream.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            os.flush();
            os.close();
            response.flushBuffer();
        } catch (ClientAbortException e) {
            log.info("用户停止下载");
            //捕获此异常表示拥护停止下载
        } catch (Exception e) {
            log.error("文件下载异常", e);
            throw new BizException(BizStatus.FILE_IO_EXCEPTION);
        }
    }

    /**
     * 保存文件
     *
     * @param file         文件
     * @param fileCategory 文件类别
     * @return {@link String}
     */
    private String saveFile(MultipartFile file, String fileCategory, Boolean keepFileName) {
        String originalFilename = file.getOriginalFilename();
        Assert.notNull(originalFilename, "文件不能为空");
        String suffix = this.getSuffix(originalFilename);
        InputStream inputStream = null;
        try {
            String month = DateUtil.format(new Date(), "yyyyMM");
            String id = IdUtil.nanoId();
            String fileName = BooleanUtil.isTrue(keepFileName) ? id + "/" + originalFilename : id + "." + suffix;
            String fileDir = "/" + fileCategory + "/" + month;
            // 保留文件原来名字
            String filePath = fileDir + "/" + fileName;
            // 开始上传
            inputStream = file.getInputStream();
            client.putObject(
                    PutObjectArgs.builder().bucket(filesProperties.getBucketName()).object(filePath).stream(
                                    inputStream, file.getSize(), -1)
                            .contentType(file.getContentType())
                            .build());
            return filePath;
        } catch (Exception e) {
            log.error("文件上传失败, originalFilename={}", originalFilename, e);
            throw new BizException(BizStatus.FILE_UPLOAD);
        } finally {
            IoUtil.close(inputStream);
        }
    }


    /**
     * url转换文件
     *
     * @param url url
     * @return {@link MultipartFile}
     */
    public MultipartFile urlToMultipartFile(String url) {
        InputStream inputStream = null;
        try {
            inputStream = new URL(url).openStream();
            String fileName = this.getFileName(url);
            return new MockMultipartFile(fileName, fileName, MimeType.getContentType(this.getSuffix(url)), inputStream);
        } catch (Exception e) {
            log.error("urlToMultipartFile失败, url={}", url, e);
            throw new BizException(BizStatus.FILE_UPLOAD);
        } finally {
            IoUtil.close(inputStream);
        }
    }

    /**
     * 获取⽂件
     *
     * @param bucketName bucket名称
     * @param objectName ⽂件名称
     * @return ⼆进制流
     */
    public InputStream getObject(String bucketName, String objectName) throws Exception {
        return client.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }

    /**
     * 获取后缀
     *
     * @param originalFilename 原始文件名
     * @return {@link String}
     */
    private String getSuffix(String originalFilename) {
        return originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
    }

    /**
     * 获取文件名字
     *
     * @param filePath 文件路径
     * @return {@link String}
     */
    private String getFileName(String filePath) {
        return filePath.substring(filePath.lastIndexOf("/") + 1);
    }

}
